Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use token list of accounts for user authorization #240

Merged
merged 10 commits into from
Feb 10, 2017
Merged

Conversation

gcampo88
Copy link
Contributor

#216

  • extracts account authorization information from the auth token in the ApiAuthenticated concern
  • uses that list of account IDs instead of querying the memberships table to determine which accounts to use
  • updates StubToken class in test_helper.rb to more closely mimic auth token behavior in terms of authorized_resources list

@codecov
Copy link

codecov bot commented Jan 31, 2017

Codecov Report

❗ No coverage uploaded for pull request head (fix/token-accounts@5cb4e8b).


Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 78368f5...5cb4e8b. Read the comment docs.

@@ -74,7 +76,7 @@ def initialize(res, scopes, explicit_user_id = nil)
end

def authorized?(r, s = nil)
resource == r.to_s && (s.nil? || scopes.include?(s.to_s))
authorized_resources.keys.include?(r) && (s.nil? || authorized_resources.values.flatten.include?(s.to_s))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line is too long. [109/100]

@@ -3,7 +3,7 @@
describe Api::AuthorizationsController do

let (:user) { create(:user) }
let (:token) { OpenStruct.new.tap { |t| t.user_id = user.id } }
let (:token) { StubToken.new(user.individual_account.id, "admin", user.id) }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer single-quoted strings when you don't need string interpolation or special symbols.

member_account.id => "member",
individual_account.id => "admin"
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

} at 17, 4 is not aligned with StubToken.new(nil, nil, user.id).tap { |t| at 12, 17 or let (:token) { StubToken.new(nil, nil, user.id).tap { |t| at 12, 2.

let (:token) { StubToken.new(nil, nil, user.id).tap { |t|
t.authorized_resources = {
member_account.id => "member",
individual_account.id => "admin"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer single-quoted strings when you don't need string interpolation or special symbols.


let (:token) { StubToken.new(nil, nil, user.id).tap { |t|
t.authorized_resources = {
member_account.id => "member",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer single-quoted strings when you don't need string interpolation or special symbols.

let (:member_account) { create(:account) }
let (:unapproved_account) { create(:account)}

let (:token) { StubToken.new(nil, nil, user.id).tap { |t|

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using {...} for multi-line blocks.
Block body expression is on the same line as the block start.

let (:random_account) { create(:account) }
before { unapproved_account.memberships.first.update!(approved: false) }
let (:member_account) { create(:account) }
let (:unapproved_account) { create(:account)}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space missing inside }.

Series.
joins('LEFT OUTER JOIN `memberships` ON `memberships`.`account_id` = `series`.`account_id`').
where(['memberships.user_id = ? and memberships.approved is true', id])
!approved_accounts.nil? ? Series.where({ account_id: approved_accounts.try(:ids) }) : []

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant curly braces around a hash parameter.

Story.
joins('LEFT OUTER JOIN `memberships` ON `memberships`.`account_id` = `pieces`.`account_id`').
where(['memberships.user_id = ? and memberships.approved is true', id])
!approved_accounts.nil? ? Story.where({ account_id: approved_accounts.try(:ids) }) : []

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant curly braces around a hash parameter.

end

def authenticate_user!
user_not_authorized unless current_user
end

def get_authorized_resources
if prx_auth_token.authorized_resources
current_user.approved_accounts ||= Account.where({ id: prx_auth_token.authorized_resources.keys })

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant curly braces around a hash parameter.
Line is too long. [104/100]

@gcampo88 gcampo88 changed the title WIP use token list of accounts for user authorization Use token list of accounts for user authorization Jan 31, 2017
@gcampo88 gcampo88 requested a review from kookster January 31, 2017 20:57
Copy link
Member

@kookster kookster left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I want a 2nd opinion on this one from Ryan, as he also worked on these authenticated controllers. @cavis can you take a look see at the code and my concerns?

def get_authorized_resources
token_accounts = prx_auth_token.authorized_resources.try(:keys)
if token_accounts
current_user.approved_accounts ||= Account.where(id: token_accounts)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we want the user model, which is retrieved from the DB and is in the model layer, also aware of the token and its contents which come from the controller layer. Seems like we are having controller / request logic and information leak into the model which should not be aware of or rely on that layer.

The other part of this is that it is could be misleading, because the user may have a different set of accounts they can access than is allowed by and listed in the token.

For example, you could generate a token with only a subset of the actual accounts the user has access to, or using client credentials, get a different account entirely in aur than is reflected in the user's account membership.

So the more I think about it, it seems like with the move to get authorized accounts from the token, not the user, they become 2 different things - there is the user that logged in, and the list of accounts authorized in the token, and they are not necessarily related except for having both been identified in the token.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious what @cavis might say about this as well - with the separation of accounts and users, where the token provides the list of accounts not the user, I think it makes sense to move these methods using the list of accounts out of the user model, perhaps into methods on the ApiAuthenticated concern, or just in the relevant controller?

Story.
joins('LEFT OUTER JOIN `memberships` ON `memberships`.`account_id` = `pieces`.`account_id`').
where(['memberships.user_id = ? and memberships.approved is true', id])
!approved_accounts.nil? ? Story.where(account_id: approved_accounts.try(:ids)) : []
Copy link
Member

@kookster kookster Jan 31, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could Story.where(account_id: approved_accounts.try(:ids)) be in a controller method instead of in the user model? doesn't seem to be using anything in the user model, except the convenience that it is the resource loaded by authorized controllers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could certainly be in there! I'm not quite sure where to place the method definitions for approved_account_series and approved_account_stories because they are called in the representers, which would lead me to want to put them in the model layer, but also are called in the controller...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, that is annoying - looks like they do get used for counts in the authorization_representer, but only that one representer at least.

Copy link
Member

@kookster kookster Feb 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can talk about it when I get in - one way to handle this may be to create an authorization resource based on the token, which could encompass both access to the user, and the list of accounts. That's sorta what feeder does, if you have a look at that, where the auth controller has this for the resource:

  def resource
    Authorization.new(prx_auth_token)
  end

And then the authorization model is basically a wrapper around the token, accessing the related user(well, user_id, feeder would have to make a remote call to getthe user), and could also return the approved_accounts and such:

class Authorization
  include HalApi::RepresentedModel

  attr_accessor :token

  def initialize(token)
    @token = token
  end

  def user_id
    token.user_id
  end
...
end

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would separate the idea of what info comes from the token and it's list of accounts vs. the user and the list of all member accounts, might be better?

@kookster kookster requested a review from cavis January 31, 2017 23:39
get_authorized_resources
end

def get_authorized_resources

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put empty method definitions on a single line.

@@ -47,7 +47,7 @@ def create_resource
story.creator_id = current_user.id
story.account_id ||= story.series.try(:account_id)
story.account_id ||= current_user.account_id
story.account_id ||= current_user.approved_accounts.first.try(:id)
story.account_id ||= current_user.approved_accounts.first.try(:id) # not sure if I should change this to look at token

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line is too long. [124/100]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in the stories controller -- not sure whether to change this to just use the first account ID listed on the auth token, or perhaps the first account ID with role "admin", or else to leave it as is -- calling the User model, which joins against memberships. (Or should this simply be the user's individual_account ? )

Copy link
Member

@kookster kookster Feb 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is the fallback for the fallback (when there is neither a series.account nor a default user account), so I think it's fine to fallback on the first account.

Creating a story doesn't require :admin permissions, so I don't think we need to check that.

There is another issue here though which is that the default account or series account may not be in the token's list of accounts, so we should check for that too - we shouldn't set the account to a value that the token does not include, does that make sense?

@gcampo88
Copy link
Contributor Author

gcampo88 commented Feb 1, 2017

@kookster this more along the lines of what you're thinking?

@@ -0,0 +1,23 @@
# encoding: utf-8

class Authorization
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this whole class.

Copy link
Member

@kookster kookster left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couple of things, smallish

create(:account, user: user, stories_count: 2)
user.approved_account_stories.count.must_equal start_count + 2
it 'has a list of stories and series' do
user.approved_account_stories.must_include story
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you leaving the User methods like approved_account_stories, even though they aren't being used anymore?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approved_account_stories and approved_account_series are both used in the Authorization Representer. Do you think that representer should be looking at the stories + series associated with the accounts on the token instead?

@@ -12,6 +12,9 @@ def current_user
def user_not_authorized
raise 'user_not_authorized'
end

def prx_auth_token

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put empty method definitions on a single line.

@@ -4,7 +4,9 @@

let(:user) { create(:user) }
let(:account) { user.default_account }
let(:representer) { Api::AuthorizationRepresenter.new(user) }
let(:token) { StubToken.new(account.id, 'admin', user.id)}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space missing inside }.

end
end

def create_resource
super.tap do |story|
story.creator_id = current_user.id
story.account_id ||= story.series.try(:account_id)
story.account_id ||= current_user.account_id
story.account_id ||= current_user.approved_accounts.first.try(:id)
story.account_id ||= current_user.account_id if authorization.authorized?(current_user.default_account)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line is too long. [109/100]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for adding that check - clever to use the authorized? method, I was thinking of doing a detect on the account list - this is much better

@token = token
end

def id
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this because the AuthorizationRepresenter wants an id to build links ... misguided?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does it want the id for? the self link? I don't think you should need to provide an id for the representer, can we track this down?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it is the self link, which is all I can think of, we can override the self_link method on the representer to not need this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug in hal gem - PRX/hal_api-rails#8

default_account.id
end

def default_account
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly, added this because AuthorizationRepresenter wants to return a default_account

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, makes sense to me

@gcampo88
Copy link
Contributor Author

gcampo88 commented Feb 3, 2017

ok, back to you @kookster !

Copy link
Member

@kookster kookster left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious if you can get rid of needing the id attribute on the Authorization model and still make the representer happy.

end
end

def create_resource
super.tap do |story|
story.creator_id = current_user.id
story.account_id ||= story.series.try(:account_id)
story.account_id ||= current_user.account_id
story.account_id ||= current_user.approved_accounts.first.try(:id)
story.account_id ||= current_user.account_id if authorization.authorized?(current_user.default_account)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for adding that check - clever to use the authorized? method, I was thinking of doing a detect on the account list - this is much better

@token = token
end

def id
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does it want the id for? the self link? I don't think you should need to provide an id for the representer, can we track this down?

@token = token
end

def id
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it is the self link, which is all I can think of, we can override the self_link method on the representer to not need this.

default_account.id
end

def default_account
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, makes sense to me

end

def token_auth_stories
Story.where(account_id: token_auth_accounts.try(:ids)) unless token_auth_accounts.nil?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you need the try when you have the unless nil? already? Fine to leave it this way, I am a belt and suspenders kind of person myself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just doing belt + suspenders!

end

it 'checks against token to see if accounts are authorized' do
authorization.authorized?(account).must_equal true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you could also write this as:
authorization.must_be :authorized?, account

just a point of information - this test is great

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants